package com.gsi.webstore.platform.storefront.internal.product;

import java.math.BigDecimal;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import com.gsi.platform.adapter.inventoryservice.v1.binding.InventoryAvailabilityStatus;
import com.gsi.webstore.platform.adapter.shoprunner.internal.product.ProductShopRunnerHelper;
import com.gsi.webstore.platform.component.catalog.capi.inventory.GSIProductStatusManager;
import com.gsi.webstore.platform.component.catalog.capi.inventory.ProductAvailabilityStatusConstants;
import com.gsi.webstore.platform.component.marketing.internal.promotion.action.descriptor.MailInRebateItemPercentageOff;
import com.gsi.webstore.platform.component.marketing.internal.promotion.action.descriptor.MailInRebateItemTargetPrice;
import com.gsi.webstore.platform.component.marketing.internal.promotion.action.descriptor.MailInRebateItemValueOff;
import com.gsi.webstore.platform.component.pricing.capi.pricetypes.PriceConstants;
import com.intershop.beehive.core.capi.component.ComponentMgr;
import com.intershop.beehive.core.capi.domain.AttributeValue;
import com.intershop.beehive.core.capi.localization.LocaleInformation;
import com.intershop.beehive.core.capi.log.Logger;
import com.intershop.beehive.core.capi.naming.NamingMgr;
import com.intershop.beehive.core.capi.request.Request;
import com.intershop.beehive.foundation.quantity.CurrencyException;
import com.intershop.beehive.foundation.quantity.Money;
import com.intershop.beehive.foundation.quantity.MoneyCalculator;
import com.intershop.beehive.xcs.capi.catalog.CatalogCategory;
import com.intershop.beehive.xcs.capi.inventory.InventoryStatus;
import com.intershop.beehive.xcs.capi.price.PriceTypeRegistry;
import com.intershop.beehive.xcs.capi.product.Product;
import com.intershop.beehive.xcs.capi.product.ProductMgr;
import com.intershop.component.catalog.capi.CatalogCategoryBO;
import com.intershop.component.foundation.capi.action.Action;
import com.intershop.component.marketing.capi.rebate.Promotion;
import com.intershop.component.marketing.capi.rebate.Rebate;
import com.intershop.component.product.capi.ProductBO;
import com.intershop.component.product.capi.ProductBOContext;
import com.intershop.component.product.capi.price.ProductPriceBO;

public class ProductBOImpl extends com.intershop.component.product.internal.ORMProductBOImpl implements
                com.gsi.webstore.platform.storefront.capi.product.ProductBO
{
    private static final String FRONTEND_AVAILABILITY_STATUS_MESSAGE = "FrontendAvailabilityStatusMessage_";
    private static final String FRONTEND_AVAILABILITY_STATUS = "FrontendAvailabilityStatus_";

    private static final String MESSAGE_IN_STOCK = "FrontendAvailabilityStatusMessage_InStock";
    private static final String MESSAGE_OUT_OF_STOCK = "FrontendAvailabilityStatusMessage_OutOfStock";
    private static final String MESSAGE_BACKORDER = "FrontendAvailabilityStatusMessage_Backorder";
    private static final String MESSAGE_ADVANCE_PRE_SELL = "FrontendAvailabilityStatusMessage_AdvancePreSell";
    private static final String MESSAGE_DISCONTINUED = "FrontendAvailabilityStatusMessage_Discontinued";
    private static final String MESSAGE_IN_STORE_ONLY = "FrontendAvailabilityStatusMessage_InStoreOnly";

    private static final String SHIPPING_WINDOW_MIN = "ShipWindowLowerBound";
    private static final String SHIPPING_WINDOW_MAX = "ShipWindowHigherBound";
    private static final String EXPECTED_DATE = "ExpectedDate";
    private static final String STREET_DATE = "StreetDate";
    private static final String PRODUCT_STATUS_IN_STORE_ONLY = "InStoreOnly";

    // define action IDs for mail-in rebates
    private static final List<String> MAIL_IN_REBATE_ACTIONS = Arrays.asList(MailInRebateItemValueOff.ID,
                    MailInRebateItemPercentageOff.ID, MailInRebateItemTargetPrice.ID);

    private final GSIProductStatusManager statusMgr = GSIProductStatusManager.getInstance();
    private final ProductMgr pMgr = NamingMgr.getManager(ProductMgr.class);

    private ComponentMgr componentMgr = null;
    private PriceTypeRegistry ptr = null;

    public ProductBOImpl(Product product, ProductBOContext context)
    {
        super(product, context);

        componentMgr = NamingMgr.getManager(ComponentMgr.class);
        ptr = (PriceTypeRegistry)componentMgr.getComponent(PriceConstants.PRICE_TYPE_REGISTRY);
    }

    /**
     * return the outgoing product plus the catalog accessories
     * 
     * @see com.gsi.webstore.platform.storefront.internal.product.ProductBOImpl#getOutgoingProductLinks()
     */
    public List<ProductBO> getOutgoingProductLinks()
    {
        List<ProductBO> bos = super.getOutgoingProductLinks();
        bos.addAll(getCatalogAccessories());
        return bos;
    }

    /**
     * collect all category accessories and product accessories and concatenate
     * the results to an iterator
     * 
     * @return
     */
    public List<ProductBO> getAccessoriesWithFallback()
    {
        List<ProductBO> bos = super.getAccessories();
        bos.addAll(getCatalogAccessories());
        return bos;
    }

    /**
     * collect accessories form categories
     * 
     * @return catalog accessories
     */
    private List<ProductBO> getCatalogAccessories()
    {
        HashSet<String> accessories = new HashSet<String>();

        @SuppressWarnings("unchecked")
        Iterator<CatalogCategory> catalogCategories = getProduct().createCatalogCategoriesIterator();

        while(catalogCategories.hasNext())
        {
            CatalogCategory cc = catalogCategories.next();
            accessories.addAll(getAccessoriesRecursive(cc));
        }

        // remove the wrapped product uuid, since a product cannot be an
        // accessory to itself
        accessories.remove(getProduct().getUUID());

        List<Product> outgoingProducts = new ArrayList<Product>();
        Iterator<String> accessoriesIt = accessories.iterator();
        while(accessoriesIt.hasNext())
        {
            String id = accessoriesIt.next();
            outgoingProducts.add(pMgr.resolveProductFromID(id, getDomain()));
        }

        return getContext().getProductBOFactory()
                        .productIteratorToProductBOs(outgoingProducts.iterator(), getContext());
    }

    /**
     * determine recursively all accessories form category to root
     * 
     * @param category
     * @return
     */
    private HashSet<String> getAccessoriesRecursive(CatalogCategory category)
    {
        if (category.isRootCategory())
        {
            return getAccessoriesForCategory(category);
        }
        else
        {
            HashSet<String> accessories = getAccessoriesForCategory(category);
            accessories.addAll(getAccessoriesRecursive(category.getParent()));
            return accessories;
        }
    }

    /**
     * get accessories for a given catalog category
     * 
     * @param category
     * @return
     */
    private HashSet<String> getAccessoriesForCategory(CatalogCategory category)
    {
        HashSet<String> accessories = new HashSet<String>();

        Iterator<String> accessoriesIt = category.getMultipleAttributes("Accessories");
        while(accessoriesIt.hasNext())
        {
            accessories.add(accessoriesIt.next());
        }
        return accessories;
    }

    /**
     * Check product availability status and return the message corresponding to
     * the state. Try first to get the message from the product, then from the
     * category path (from the leaf to the root) of default category and finally
     * from the repository. Since availability message is localizable, the
     * locale from current request is assumed
     * 
     * @return availability message
     */
    public String getAvailabilityMessage()
    {
        return getAvailabilityMessage(Request.getCurrent().getLocale());
    }

    /**
     * Check product availability status and return the message corresponding to
     * the state. Try first to get the message from the product, then from the
     * category path (from the leaf to the root) of default category and finally
     * from the repository
     * 
     * @param locale the locale of availability message
     * @return availability message
     */
    public String getAvailabilityMessage(LocaleInformation locale)
    {
        String availabilityMessage = null;

        String frontendStatus = statusMgr.calculateProductAvailabilityStatus(getProduct()).getFrontendStatus();
        if (null != frontendStatus)
        {
            AttributeValue av = getProduct().getAttributeValue(FRONTEND_AVAILABILITY_STATUS_MESSAGE + frontendStatus,
                            locale);

            if (av != null && !av.getStringValueNull())
            {
                availabilityMessage = av.getStringValue();
            }
            else
            {
                // fallback to the default category path messages
                if (getDefaultCatalogCategory() != null)
                {
                    Map<String, ? extends CatalogCategoryBO> categoryPath = getDefaultCatalogCategory()
                                    .getCatalogCategoryPath();
                    Set<String> keys = categoryPath.keySet();
                    ListIterator<String> categoryNames = new ArrayList<String>(keys).listIterator(keys.size());
                    while(categoryNames.hasPrevious())
                    {
                        String categoryName = categoryNames.previous();
                        availabilityMessage = categoryPath.get(categoryName).getString(
                                        FRONTEND_AVAILABILITY_STATUS_MESSAGE + frontendStatus, locale);
                        if (availabilityMessage != null)
                        {
                            break;
                        }
                    }
                    // List<? extends CatalogCategoryBO> categoryPath =
                    // getDefaultCatalogCategory()
                    // .getCatalogCategoryPath();
                    // ListIterator<? extends CatalogCategoryBO> categories =
                    // categoryPath.listIterator(categoryPath
                    // .size());
                    // while(categories.hasPrevious())
                    // {
                    // CatalogCategoryBO category = categories.previous();
                    // availabilityMessage =
                    // category.getString(FRONTEND_AVAILABILITY_STATUS_MESSAGE +
                    // frontendStatus,
                    // locale);
                    // if (availabilityMessage != null)
                    // {
                    // break;
                    // }
                    // }
                }
                if (availabilityMessage == null)
                {
                    // fallback to the repository messages
                    av = getRepository().getAttributeValue(FRONTEND_AVAILABILITY_STATUS_MESSAGE + frontendStatus,
                                    locale);
                    if (av != null && !av.getStringValueNull())
                    {
                        availabilityMessage = av.getStringValue();
                    }
                }
            }
        }
        return availabilityMessage;
    }

    /**
     * Check product availability status and return the message corresponding to
     * the state. Try first to get the message from the product, then from the
     * category path (from the leaf to the root) of default category and finally
     * from the repository. Since availability message is localizable, the
     * locale from current request is assumed
     * 
     * @return availability message
     */
    public String getAvailabilityDateMessage()
    {
        return getAvailabilityDateMessage(Request.getCurrent().getLocale());
    }

    /**
     * Check product availability status and return the message corresponding to
     * the state. Try first to get the message from the product, then from the
     * category path (from the leaf to the root) of default category and finally
     * from the repository
     * 
     * @param locale the locale of availability message
     * @return availability message
     */
    public String getAvailabilityDateMessage(LocaleInformation locale)
    {
        String availabilityMessage = null;

        Product product = getProduct();
        InventoryAvailabilityStatus inventoryAvailabilityStatus = statusMgr.getInventoryAvailabilityStatus(product);
        InventoryStatus inventoryStatus = statusMgr.getInventoryStatus(product);

        Date expectedDate = product.getDate(EXPECTED_DATE);
        Date streetDate = product.getDate(STREET_DATE);
        Date logicalDate = getLogicalDate(expectedDate, streetDate);
        String logicalDateStr = "";
        if (logicalDate != null)
        {
            DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.SHORT);
            logicalDateStr = dateFormatter.format(logicalDate);
        }

        Boolean inStoreOnlyAttribute = product.getBoolean(PRODUCT_STATUS_IN_STORE_ONLY);
        boolean isInStoreOnly = false;
        if (inStoreOnlyAttribute != null)
        {
            isInStoreOnly = inStoreOnlyAttribute.booleanValue();
        }

        Integer shippingWindowMinHour = product.getInteger(SHIPPING_WINDOW_MIN);
        Integer shippingWindowMaxHour = product.getInteger(SHIPPING_WINDOW_MAX);
        String shippingWindow = getShipWindowMessage(shippingWindowMinHour, shippingWindowMaxHour);

        if (ProductAvailabilityStatusConstants.OMS_STATUS_REGULAR.equals(inventoryAvailabilityStatus.getOmsStatus()))
        {
            if (inventoryStatus.isInStock())
            {
                if (ProductAvailabilityStatusConstants.CATALOG_CLASS_REGULAR.equals(inventoryAvailabilityStatus
                                .getCatalogClass()))
                {
                    if (ProductAvailabilityStatusConstants.SALES_CLASS_STOCK.equals(inventoryAvailabilityStatus
                                    .getSalesClass()))
                    {
                        if (isLogicalDateLowerCurrent(logicalDate))
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_IN_STOCK, shippingWindow);
                        }
                        else
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_ADVANCE_PRE_SELL,
                                            logicalDateStr);
                        }
                    }
                    else
                    {
                        if (isLogicalDateLowerCurrent(logicalDate))
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_BACKORDER, shippingWindow);
                        }
                        else
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_ADVANCE_PRE_SELL,
                                            logicalDateStr);
                        }
                    }
                }
                else if (ProductAvailabilityStatusConstants.CATALOG_CLASS_ALWAYS.equals(inventoryAvailabilityStatus
                                .getCatalogClass()))
                {
                    if (isLogicalDateLowerCurrent(logicalDate))
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_IN_STOCK, shippingWindow);
                    }
                    else
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_ADVANCE_PRE_SELL, logicalDateStr);
                    }
                }
            }
            else
            {
                if (ProductAvailabilityStatusConstants.SALES_CLASS_STOCK.equals(inventoryAvailabilityStatus
                                .getSalesClass()))
                {
                    if (isLogicalDateLowerCurrent(logicalDate))
                    {
                        if (!isInStoreOnly)
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_OUT_OF_STOCK, shippingWindow);
                        }
                        else
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_IN_STORE_ONLY,
                                            shippingWindow);
                        }
                    }
                    else
                    {
                        if (!isInStoreOnly)
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_ADVANCE_PRE_SELL,
                                            logicalDateStr);
                        }
                        else
                        {
                            availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_IN_STORE_ONLY, ""
                                            + getStatusMessage(MESSAGE_ADVANCE_PRE_SELL, locale) + logicalDateStr);
                        }
                    }
                }
                else if (ProductAvailabilityStatusConstants.SALES_CLASS_ADV_SALE_OPEN
                                .equals(inventoryAvailabilityStatus.getSalesClass()))
                {
                    if (isLogicalDateLowerCurrent(logicalDate))
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_BACKORDER, shippingWindow);
                    }
                    else
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_BACKORDER, logicalDateStr);
                    }
                }
                else if (ProductAvailabilityStatusConstants.SALES_CLASS_ADV_SALE_LIMITED
                                .equals(inventoryAvailabilityStatus.getSalesClass()))
                {
                    if (isLogicalDateLowerCurrent(logicalDate))
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_BACKORDER, shippingWindow);
                    }
                    else
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_ADVANCE_PRE_SELL, logicalDateStr);
                    }
                }
            }
        }
        else if (ProductAvailabilityStatusConstants.OMS_STATUS_DISCONTINUED.equals(inventoryAvailabilityStatus
                        .getOmsStatus()))
        {
            if (ProductAvailabilityStatusConstants.CATALOG_CLASS_REGULAR.equals(inventoryAvailabilityStatus
                            .getCatalogClass()))
            {
                if (ProductAvailabilityStatusConstants.SALES_CLASS_STOCK.equals(inventoryAvailabilityStatus
                                .getSalesClass()))
                {
                    availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_DISCONTINUED, " "
                                    + getStatusMessage(MESSAGE_IN_STOCK, locale) + shippingWindow);
                }
                else
                {
                    availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_DISCONTINUED, " "
                                    + getStatusMessage(MESSAGE_BACKORDER, locale) + shippingWindow);
                }
            }
            else if (ProductAvailabilityStatusConstants.CATALOG_CLASS_ALWAYS.equals(inventoryAvailabilityStatus
                            .getCatalogClass()))
            {
                if (inventoryStatus.isInStock())
                {
                    availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_DISCONTINUED, "");
                }
                else
                {
                    if (!isInStoreOnly)
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_DISCONTINUED, " "
                                        + getStatusMessage(MESSAGE_IN_STORE_ONLY, locale));
                    }
                    else
                    {
                        availabilityMessage = getCombinedStatusMessage(locale, MESSAGE_DISCONTINUED, " "
                                        + getStatusMessage(MESSAGE_IN_STOCK, locale) + shippingWindow);
                    }
                }
            }
        }
        return availabilityMessage;
    }

    private String getCombinedStatusMessage(LocaleInformation locale, String statusMessage, String dateMessage)
    {
        String availabilityMessage = getStatusMessage(statusMessage, locale);
        if (availabilityMessage == null)
        {
            return null;
        }
        return availabilityMessage + " " + dateMessage;
    }

    private String getStatusMessage(String frontendStatusMessage, LocaleInformation locale)
    {
        String statusMessage = null;

        AttributeValue av = getProduct().getAttributeValue(frontendStatusMessage, locale);

        if (av != null && !av.getStringValueNull())
        {
            statusMessage = av.getStringValue();
        }
        else
        {
            // fallback to the default category path messages
            if (getDefaultCatalogCategory() != null)
            {
                Map<String, ? extends CatalogCategoryBO> categoryPath = getDefaultCatalogCategory()
                                .getCatalogCategoryPath();
                Set<String> keys = categoryPath.keySet();
                ListIterator<String> categoryNames = new ArrayList<String>(keys).listIterator(keys.size());
                while(categoryNames.hasPrevious())
                {
                    String categoryName = categoryNames.previous();
                    statusMessage = categoryPath.get(categoryName).getString(frontendStatusMessage, locale);
                    if (statusMessage != null)
                    {
                        break;
                    }
                }
            }
            if (statusMessage == null)
            {
                // fallback to the repository messages
                av = getRepository().getAttributeValue(frontendStatusMessage, locale);
                if (av != null && !av.getStringValueNull())
                {
                    statusMessage = av.getStringValue();
                }
            }
        }
        return statusMessage;
    }

    public String getShipWindowMessage()
    {
        Product product = getProduct();
        Integer shippingWindowMinHour = product.getInteger(SHIPPING_WINDOW_MIN);
        Integer shippingWindowMaxHour = product.getInteger(SHIPPING_WINDOW_MAX);
        
        return getShipWindowMessage(shippingWindowMinHour, shippingWindowMaxHour);
    }
    
    private String getShipWindowMessage(Integer minHour, Integer maxHour)
    {
        String shipWindowMessage = "";
        if (minHour != null && maxHour != null)
        {
            if (minHour < 24 || maxHour < 24)
            {
                shipWindowMessage = minHour + " - " + maxHour + " Hours";
            }
            else
            {
                if (minHour > 120 && maxHour > 120)
                {
                    shipWindowMessage = (minHour / 120) + " - " + ((maxHour + 119) / 120) + " Weeks";
                }
                else
                {
                    shipWindowMessage = (minHour / 24) + " - " + ((maxHour + 23) / 24) + " Days";
                }
            }
        }
        return shipWindowMessage;
    }

    private Date getLogicalDate(Date expectedDate, Date streetDate)
    {
        if (expectedDate == null && streetDate != null)
        {
            return streetDate;
        }

        if (expectedDate != null && streetDate == null)
        {
            return expectedDate;
        }

        if (expectedDate != null && streetDate != null)
        {
            if (expectedDate.after(streetDate))
            {
                return expectedDate;
            }
            else
            {
                return streetDate;
            }
        }
        return null;
    }

    private boolean isLogicalDateLowerCurrent(Date logicalDate)
    {
        if (logicalDate != null)
        {
            Calendar calendarLogicalDate = Calendar.getInstance();
            Calendar calendarCurrentDate = Calendar.getInstance();
            calendarLogicalDate.setTime(logicalDate);
            calendarCurrentDate.setTime(new Date());
            if (calendarLogicalDate.before(calendarCurrentDate))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Gets shipping window lower bound. The method searches for lower bound
     * value at the product first, then at product category path (from the leaf
     * to the root) of default category and finally at product repository
     * 
     * @return ship window lower bound
     */
    public String getShippingWindowLowerBound()
    {
        AttributeValue av = getProduct().getAttributeValue("ShipWindowLowerBound");

        if (av != null && !av.getIntValueNull())
        {
            return Integer.toString(av.getIntValue());
        }

        String lowerBound = null;
        String frontendStatus = statusMgr.calculateProductAvailabilityStatus(getProduct()).getFrontendStatus();

        if (null != frontendStatus)
        {
            // fallback to the default category path lower bound
            Integer lowerBoundInteger = null;
            if (getDefaultCatalogCategory() != null)
            {
                Map<String, ? extends CatalogCategoryBO> categoryPath = getDefaultCatalogCategory()
                                .getCatalogCategoryPath();
                Set<String> keys = categoryPath.keySet();
                ListIterator<String> categoryNames = new ArrayList<String>(keys).listIterator(keys.size());
                while(categoryNames.hasPrevious())
                {
                    String categoryName = categoryNames.previous();
                    lowerBoundInteger = categoryPath.get(categoryName).getInteger(
                                    FRONTEND_AVAILABILITY_STATUS + frontendStatus + "@ShipWindowLowerBound");
                    if (lowerBoundInteger != null)
                    {
                        break;
                    }
                }
                // List<? extends CatalogCategoryBO> categoryPath =
                // getDefaultCatalogCategory().getCatalogCategoryPath();
                // ListIterator<? extends CatalogCategoryBO> categories =
                // categoryPath.listIterator(categoryPath.size());
                // while(categories.hasPrevious())
                // {
                // CatalogCategoryBO category = categories.previous();
                // lowerBoundInteger =
                // category.getInteger(FRONTEND_AVAILABILITY_STATUS +
                // frontendStatus
                // + "@ShipWindowLowerBound");
                // if (lowerBoundInteger != null)
                // {
                // break;
                // }
                // }
            }
            if (lowerBoundInteger != null)
            {
                lowerBound = Integer.toString(lowerBoundInteger);
            }
            else
            {
                // fallback to the repository lower bound
                av = getRepository().getAttributeValue(
                                FRONTEND_AVAILABILITY_STATUS + frontendStatus + "@ShipWindowLowerBound");
                if (av != null && !av.getIntValueNull())
                {
                    lowerBound = Integer.toString(av.getIntValue());
                }
            }
        }
        return lowerBound;
    }

    /**
     * Gets shipping window higher bound. The method searches for higher bound
     * value at the product first, then at product category path (from the leaf
     * to the root) of default category and finally at product repository
     * 
     * @return ship window higher bound
     */
    public String getShippingWindowHigherBound()
    {
        AttributeValue av = getProduct().getAttributeValue("ShipWindowHigherBound");

        if (av != null && !av.getIntValueNull())
        {
            return Integer.toString(av.getIntValue());
        }

        String higherBound = null;
        String frontendStatus = statusMgr.calculateProductAvailabilityStatus(getProduct()).getFrontendStatus();

        if (null != frontendStatus)
        {
            // fallback to the default category path higher bound
            Integer higherBoundInteger = null;
            if (getDefaultCatalogCategory() != null)
            {
                Map<String, ? extends CatalogCategoryBO> categoryPath = getDefaultCatalogCategory()
                                .getCatalogCategoryPath();
                Set<String> keys = categoryPath.keySet();
                ListIterator<String> categoryNames = new ArrayList<String>(keys).listIterator(keys.size());
                while(categoryNames.hasPrevious())
                {
                    String categoryName = categoryNames.previous();
                    higherBoundInteger = categoryPath.get(categoryName).getInteger(
                                    FRONTEND_AVAILABILITY_STATUS + frontendStatus + "@ShipWindowHigherBound");
                    if (higherBoundInteger != null)
                    {
                        break;
                    }
                }
                // List<? extends CatalogCategoryBO> categoryPath =
                // getDefaultCatalogCategory().getCatalogCategoryPath();
                // ListIterator<? extends CatalogCategoryBO> categories =
                // categoryPath.listIterator(categoryPath.size());
                // while(categories.hasPrevious())
                // {
                // CatalogCategoryBO category = categories.previous();
                // higherBoundInteger =
                // category.getInteger(FRONTEND_AVAILABILITY_STATUS +
                // frontendStatus
                // + "@ShipWindowHigherBound");
                // if (higherBoundInteger != null)
                // {
                // break;
                // }
                // }
            }
            if (higherBoundInteger != null)
            {
                higherBound = Integer.toString(higherBoundInteger);
            }
            else
            {
                // fallback to the repository higher bound
                av = getRepository().getAttributeValue(
                                FRONTEND_AVAILABILITY_STATUS + frontendStatus + "@ShipWindowHigherBound");
                if (av != null && !av.getIntValueNull())
                {
                    higherBound = Integer.toString(av.getIntValue());
                }
            }
        }
        return higherBound;
    }

    /**
     * 
     * @return The Product ShopRunner eligibility.
     */
    public boolean isShopRunnerEligible()
    {
        return ProductShopRunnerHelper.isShopRunnerEligible(this.getProduct());
    }

    @Override
    public boolean isAgeRestricted()
    {
        return getProduct().getInteger("LegalPurchaseAge") != null;
    }

    @Override
    public Integer getLegalPurchaseAge()
    {
        return getProduct().getInteger("LegalPurchaseAge");
    }

    @Override
    public String getAgeRating()
    {
        return getProduct().getLocalizedString("AgeRating", Request.getCurrent().getLocale());
    }

    @Override
    public String getAgeRating(LocaleInformation aLocale)
    {
        return getProduct().getLocalizedString("AgeRating", aLocale);
    }

    @Override
    public Money getWasPrice()
    {
        return getProductPrice(PriceConstants.WAS_PRICE_TYPE).getPrice();
    }

    @Override
    public Money getMinimumWasPrice()
    {
        ProductPriceBO entry = getProductPrice(PriceConstants.WAS_PRICE_TYPE);

        if (entry != null && entry.isRange())
        {
            return entry.getMinimum().getPrice();
        }

        entry = getProductPrice(ptr.getDefaultPriceTypeRegistryName());

        if (entry != null && entry.isRange())
        {
            return entry.getMinimum().getPrice();
        }

        return getWasPrice();
    }

    @Override
    public Money getMaximumWasPrice()
    {
        ProductPriceBO entry = getProductPrice(PriceConstants.WAS_PRICE_TYPE);

        if (entry != null && entry.isRange())
        {
            return entry.getMaximum().getPrice();
        }

        entry = getProductPrice(ptr.getDefaultPriceTypeRegistryName());

        if (entry != null && entry.isRange())
        {
            return entry.getMaximum().getPrice();
        }

        return getWasPrice();
    }

    /**
     * Returns the mail-in rebate promotion
     * 
     * @param applicablePromotions applicable promotions
     * @return mail-in rebate promotion
     */
    public Promotion getMailInRebate(Collection<Promotion> applicablePromotions)
    {
        Promotion mailInRebate = null;
        Action action;
        
        // check applicable promotions for a mail-in rebate action
        for (Promotion promotion : applicablePromotions)
        {
            action = getMailInRebateAction(promotion);
            if (action != null)
            {
                // found mail-in rebate action: take that promotion
                mailInRebate = promotion;
                break;
            }
        }
        return mailInRebate;
    }

    /**
     * Tries to get a mail-in rebate action for the given promotion
     * 
     * @param aPromotion a promotion
     * @return a mail-in rebate action
     */
    private Action getMailInRebateAction(Promotion aPromotion)
    {
        Iterator<Rebate> rebates;
        Iterator<Action> actions;
        Rebate rebate;
        Action action;
        
        // check all rebates
        rebates = aPromotion.createRebatesIterator();
        while (rebates.hasNext())
        {
            rebate = rebates.next();
            
            // and check all rebate actions
            actions = rebate.createActionsIterator();
            while (actions.hasNext())
            {
                action = actions.next();
                if (MAIL_IN_REBATE_ACTIONS.contains(action.getActionDescriptorID()))
                {
                    // is defined in list of mail-in rebate actions
                    return action;
                }
            }
        }
        // no mail-in rebate action found
        return null;
    }

    public Money getDiscountAmount(Promotion aPromotion)
    {
        // set default return value
        Money discount = Money.NOT_AVAILABLE;
        
        if (aPromotion != null)
        {
            // get mail-in rebate action
            Action action = getMailInRebateAction(aPromotion);

            // calculate discount
            discount = getDiscountAmount(action);
            
        }
        // return calculated discount
        return discount;
    }
    
    /**
     * Returns base price for discount calculation. <br/>
     * Usually this is the sale price.
     * 
     * @return discount base price
     */
    private Money getDiscountBasePrice()
    {
        ProductPriceBO basePrice = this.getProductPrice(PriceConstants.SALE_PRICE_TYPE);
        return basePrice==null ? Money.NOT_AVAILABLE : basePrice.getPrice();
    }
    
    /**
     * Calculates the discount amount.
     * 
     * @param action a rebate action
     * @param productPrice a product price
     * @return discount amount
     */
    private Money getDiscountAmount(Action action)
    {
        Money discount = Money.NOT_AVAILABLE;
        
        // get product price
        Money productPrice = getDiscountBasePrice();
        
        // money calculator
        MoneyCalculator mc = MoneyCalculator.createHighPrecisionCalculator();
        
        if (action != null && productPrice != null && productPrice.isAvailable())
        {
            // get action descriptor ID
            String actionDescriptorID = action.getActionDescriptorID();
            
            // calculate value off
            if (actionDescriptorID.equals(MailInRebateItemValueOff.ID))
            {
                Money valueOff = MailInRebateItemValueOff.getValueOff(action);
                discount = valueOff != null ? valueOff : Money.NOT_AVAILABLE;
                discount = mc.multiply(discount, new BigDecimal(-1));
            } else if (actionDescriptorID.equals(MailInRebateItemPercentageOff.ID))
            {
                Double percentage = MailInRebateItemPercentageOff.getPercentageOff(action);
                if (percentage != null)
                {
                    discount = mc.multiply(productPrice, new BigDecimal(percentage));
                    discount = mc.multiply(discount, new BigDecimal(-1));
                }
            } else if (actionDescriptorID.equals(MailInRebateItemTargetPrice.ID))
            {
                Money targetPrice = MailInRebateItemTargetPrice.getTargetPrice(action);
                discount = subtract(targetPrice, productPrice);
            }
        }
        return discount;
    }
    
    /**
     * Subtraction of money values
     * 
     * @param minuend term one of subtraction
     * @param subtrahend term two of subtraction
     * @return subtraction result
     */
    private Money subtract(Money minuend, Money subtrahend)
    {
        Money difference = Money.NOT_AVAILABLE;
        try
        {
            difference = MoneyCalculator.createHighPrecisionCalculator().subtract(minuend, subtrahend);
        }
        catch(CurrencyException e)
        {
            Logger.error(this, "Error calculating discounts", e);
        }
        return difference;
    }
    
    /**
     * Addition of money values
     * 
     * @param term 1 of addition
     * @param term 2 of addition
     * @return addition result
     */
    private Money add(Money term1, Money term2)
    {
        Money sum = Money.NOT_AVAILABLE;
        try
        {
            sum = MoneyCalculator.createHighPrecisionCalculator().add(term1, term2);
        }
        catch(CurrencyException e)
        {
            Logger.error(this, "Error calculating discounts", e);
        }
        return sum;
    }
    
    public Money getDiscountedPrice(Promotion aPromotion)
    {
        Money discountedPrice = Money.NOT_AVAILABLE;

        // calculate discount 
        Money discount = getDiscountAmount(aPromotion);
        
        // get product price
        Money productPrice = getDiscountBasePrice();
        
        // calculate discounted price
        if (discount.isAvailable() && productPrice.isAvailable())
        {
            discountedPrice = add(getDiscountBasePrice(), discount);
        }
        // return calculated value
        return discountedPrice;
    }

}
